Skip to content

feat(vm): IO — byte/char reads, gets limit/chomp, pread/pwrite, half-close - #347

Merged
tannevaled merged 4 commits into
mainfrom
feat/io-descriptors
Aug 10, 2026
Merged

feat(vm): IO — byte/char reads, gets limit/chomp, pread/pwrite, half-close#347
tannevaled merged 4 commits into
mainfrom
feat/io-descriptors

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Horodate: 2026-08-10 10:41 CEST

Advances the IO / File (as IO) / StringIO read+write surface toward MRI 3.4/4.0 on the existing buffer-backed IOObj model, with per-branch tests and a ruby/spec ratchet bump.

What's implemented

Shared (IO, File, StringIO):

  • getbyte / readbyte / readchar (EOFError at EOF)
  • ungetbyte / ungetc (Integer + String pushback, nil no-op, TypeError)
  • each_byte / each (return self)
  • sysread (length + output-buffer arg, EOFError, negative → ArgumentError) / syswrite
  • gets / readline / readlines / each_line now honour (sep, limit, chomp:), with universal-newline chomp
  • #lineno / #lineno= (advanced by each successful line read)
  • close_read / close_write / closed_read? / closed_write? (both halves shut ⇒ fully closed; close_write flushes)
  • Readable/writable guards: IOError "not opened for reading|writing", "closed stream"

IO + File only (not StringIO):

  • pread / pwrite (absolute offset, cursor untouched; Errno::EINVAL, EOFError, FrozenError on a frozen buffer)
  • sysseek (returns the resulting position)
  • binmode? / autoclose? / autoclose= / fdatasync

Correctness fix: readlines / each_line / each / IO.foreach / IO.readlines now raise ArgumentError "invalid limit: 0 for <meth>" for an explicit 0 limit (a 0-byte line never advances the cursor — the limit support otherwise looped forever on core/io/{each_line,foreach}_spec). A single #gets(0) still returns "". The foreach/readlines class methods also gained (sep, limit) pass-through.

Deferred

  • #fileno (no real fd in the buffer-backed model; StringIO returns nil, IO an Integer — fabricating an fd was judged worse than omitting)
  • Non-duplex close_read/close_write raise ("closing non-duplex IO")
  • IO.select blocking/threaded corners, tty/ioctl

Verification

  • go test ./...: PASS (8 packages)
  • Whole-package CI-exact coverage gate: only the pre-existing tolerated partials, none new (registerIOClassMethods reached 100%)
  • New code 100% incl error branches; gofmt + go vet clean; no prelude.rb change
  • ruby/spec ratchet: 12517 passing (baseline 12408, +109); FLOOR bumped 12378 → 12487 (N−30 margin). core/io/{each_line,foreach,readlines} + StringIO specs that previously hung now complete with higher pass counts.

🤖 Generated with Claude Code

tannevaled and others added 4 commits August 10, 2026 10:12
…close

Horodate: 2026-08-10 10:12 CEST

Advance the IO / File / StringIO read+write surface toward MRI 3.4/4.0 on
the existing buffer-backed IOObj model.

Shared (IO, File, StringIO):
  - getbyte / readbyte / readchar (EOFError at EOF)
  - ungetbyte / ungetc (Integer + String pushback, nil no-op, TypeError)
  - each_byte / each (return self)
  - sysread (len + output buffer, EOFError, negative -> ArgumentError) / syswrite
  - gets/readline/readlines/each_line now honour (sep, limit, chomp:)
  - #lineno / #lineno= (advanced by each successful line read)
  - close_read / close_write / closed_read? / closed_write?
    (both halves shut -> fully closed; write flushes)
  - readable/writable guards: IOError "not opened for reading|writing"

IO + File only (not StringIO):
  - pread / pwrite (absolute offset, cursor untouched; Errno::EINVAL,
    EOFError, FrozenError buffer)
  - sysseek (returns resulting position)
  - binmode? / autoclose? / autoclose= / fdatasync

Deferred: fileno (no real fd in the buffer model), non-duplex close raise,
IO.select threaded corners.

Verification:
  - go test ./internal/vm/ -run TestIODescriptors: PASS
  - new code 100% incl error branches (per-func cover)
  - gofmt + go vet clean; no prelude.rb change

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Horodate: 2026-08-10 10:34 CEST

readlines / each_line / each / IO.foreach / IO.readlines now raise
ArgumentError "invalid limit: 0 for <meth>" for an explicit 0 byte limit,
matching MRI. A 0-byte line never advances the cursor, so the earlier
limit support looped forever on core/io/{each_line,foreach}_spec. A single
#gets(0) still returns "" (single call, no loop). foreach/readlines class
methods also gained (sep, limit) pass-through.

Verification:
  - core/io/{each_line,foreach,readlines}_spec + stringio specs now
    complete (were hanging) with higher pass counts
  - go test ./... PASS; whole-package cover gate: only tolerated partials
    (registerIOClassMethods now 100%)
  - gofmt + go vet clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Horodate: 2026-08-10 10:40 CEST

Measured 12517 passing after the IO read/write surface work (N-30 margin
for the known ratchet infra-flake). Keep the higher FLOOR on any merge
conflict with a concurrent File/Dir PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	scripts/conformance/rubyspec/FLOOR
@tannevaled
tannevaled merged commit 0fa5ee5 into main Aug 10, 2026
5 of 7 checks passed
@tannevaled
tannevaled deleted the feat/io-descriptors branch August 10, 2026 09:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant